using UnityEngine;
using UnityEditor;


using System;
using System.Collections.Generic;
using System.Linq;

using Prota.Unity;

namespace Prota.Editor
{

    [CustomEditor(typeof(ProceduralMesh))]
    public class ProceduralMeshEditor : UnityEditor.Editor
    {
        EditorPrefEntryBool editMode = new EditorPrefEntryBool("ProceduralMeshEditor.editMode", false);
        EditorPrefEntryBool showInnerEdges = new EditorPrefEntryBool("ProceduralMeshEditor.showInnerEdges", true);
        EditorPrefEntryBool showEdges = new EditorPrefEntryBool("ProceduralMeshEditor.showOuterEdges", true);
        EditorPrefEntryBool showBounds = new EditorPrefEntryBool("ProceduralMeshEditor.showBounds", true);
        
        new ProceduralMesh target => base.target as ProceduralMesh;
        
        const float pointSelectionRangeInPixel = 20f;
        const float edgeSelectionRangeInPixel = 60f;
        
        
        
        const int handleStart = 400;
        
        int? highlightVertexId = null;
        int? highlightEdgeId = null;
        bool needRepaint = false;
        EventType curEvent;
        
        Vector2? prevMousePos = null;
        int? controllingId = null;
        
        
        int C2I(int controlId) => controlId - handleStart;
        int I2C(int pointIndex) => pointIndex + handleStart;
        
        bool needUpdateMesh = false;
        Vector3[] vertices
        {
            get => target.vertices;
            set => target.vertices = value;
        }
        
        int[] triangles => target.triangles;
        
        public override void OnInspectorGUI()
        {
            Undo.RecordObject(target, "ProceduralMesh");
            
            if(GUILayout.Button("Rebuild Mesh"))
            {
                foreach(var s in Resources.FindObjectsOfTypeAll<ProceduralMesh>())
                    s.RebuildMesh();
            }
            
            if(Event.current.isKey)
            {
                if(Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.X)
                {
                    editMode.value = !editMode.value;
                    Event.current.Use();
                }
                else
                {
                    editMode.value = false;
                }
            }
            
            if(editMode.value = EditorGUILayout.Toggle("Edit Mode", editMode.value))
            {
                Tools.current = Tool.None;
            }
            
            showEdges.value = EditorGUILayout.Toggle("Show Edges", showEdges.value);
            if(showEdges.value) showInnerEdges.value = EditorGUILayout.Toggle("Show Inner Edges", showInnerEdges.value);
            showBounds.value = EditorGUILayout.Toggle("Show Bounds", showBounds.value);
            
            var oriColor = target.vertexColor;
            base.OnInspectorGUI();
            if(target.vertexColor != oriColor)
            {
                target.RebuildMesh();
            }
        }
        
        void OnSceneGUI()
        {
            needRepaint = false;
            var c = Handles.color;
            curEvent = Event.current.type;
            if(this.IsLayoutEvent()) SyncSelection();
            if(editMode.value) ProcessMouseOperation();
            if(this.IsRepaintOrLayoutEvent()) DrawVerticesHandles();
            if(this.IsRepaintOrLayoutEvent()) DrawEdgeHandles();
            if(this.IsRepaintEvent() && showBounds.value) DrawBounds();
            Handles.color = c;
            if(needUpdateMesh)
            {
                target.RebuildTriangleArray();
                target.RebuildMesh();
            }
            if(needRepaint || Event.current.type == EventType.MouseMove) SceneView.RepaintAll();
        }

        private void DrawBounds()
        {
            var bounds = target.mesh.bounds;
            bounds.center += target.transform.position;
            Handles.color = Color.yellow;
            Handles.DrawWireCube(bounds.center, bounds.size.WithZ(0));
        }


        private void ProcessMouseOperation()
        {
            switch(Event.current.type)
            {
                case EventType.MouseDown:
                {
                    prevMousePos = Event.current.mousePosition;
                    Event.current.Use();
                    if(!HandleUtility.nearestControl.In(handleStart, handleStart + vertices.Length - 1)) return;
                    controllingId = HandleUtility.nearestControl;
                    break;
                }
                case EventType.MouseUp:
                {
                    if(Event.current.mousePosition.To(prevMousePos.Value).magnitude < 10f)
                    {
                        if(Event.current.button == 0)
                        {
                            if(highlightEdgeId != null)
                            {
                                var edge = highlightEdgeId.Value;
                                var vertA = target.VertexWorldPos(edge);
                                var vertB = target.VertexWorldPos(edge + 1);
                                var vertNew = ClosetPoint(vertA, vertB) - target.transform.position.ToVec2();
                                
                                var list = vertices.ToList();
                                list.Insert(edge + 1, vertNew);
                                vertices = list.ToArray();
                                
                                needUpdateMesh = true;
                            }
                        }
                        else if(Event.current.button == 1)
                        {
                            if(highlightVertexId != null)
                            {
                                if(vertices.Length <= 3) return;
                                var vertexId = highlightVertexId.Value;
                                vertices = vertices.Take(vertexId).Concat(vertices.Skip(vertexId + 1)).ToArray();
                                needUpdateMesh = true;
                            }
                        }
                    }
                    
                    
                    if(controllingId == null) return;
                    controllingId = null;
                    prevMousePos = null;
                    needUpdateMesh = true;
                    Event.current.Use();
                    break;
                }
                case EventType.MouseDrag:
                {
                    if(controllingId == null) return;
                    var move = Event.current.delta;
                    var vertexId = C2I(controllingId.Value);
                    var moveFromWorldSpace = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition).HitXYPlane();
                    var moveToWorldSpace = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition + move).HitXYPlane();
                    var moveFromLocalSpace = target.transform.InverseTransformPoint(moveFromWorldSpace);
                    var moveToLocalSpace = target.transform.InverseTransformPoint(moveToWorldSpace);
                    var moveInLocalSpace = moveToLocalSpace - moveFromLocalSpace;
                    vertices[vertexId] += moveInLocalSpace;
                    prevMousePos += move;
                    needRepaint = true;
                    Event.current.Use();
                    break;
                }
                default: return;
            }
        }

        private void DrawVerticesHandles()
        {
            for(int i = 0; i < vertices.Length; i++)
            {
                var p = target.VertexWorldPos(i);
                Handles.color = Color.blue;
                bool selected = highlightVertexId == i;
                if(selected) Handles.color = Color.red;
                else Handles.color = Color.blue;
                Handles.DotHandleCap(I2C(i), p, Quaternion.identity, 4f / WorldDistanceToGUIDistance(1), curEvent);
            }
        }
        
        float WorldDistanceToGUIDistance(float d)
        {
            var a = HandleUtility.WorldToGUIPoint(Vector3.zero);
            var b = HandleUtility.WorldToGUIPoint(Vector3.right);
            return Vector2.Distance(a, b) * d;
        }
        
        static Vector3[] polyline = new Vector3[2];
        Vector2 ClosetPoint(Vector2 p1, Vector2 p2)
        {
            polyline[0] = p1;
            polyline[1] = p2;
            return HandleUtility.ClosestPointToPolyLine(polyline);
        }
        
        
        static HashSet<Vector2Int> edges = new();
        private void DrawEdgeHandles()
        {
            for(int i = 0; i < vertices.Length; i++)
            {
                var p1 = target.VertexWorldPos(i);
                var p2 = target.VertexWorldPos(i + 1);
                Handles.color = Color.green;
                bool selected = highlightEdgeId == i;
                if(selected) Handles.color = Color.red;
                else Handles.color = Color.green;
                if(showEdges.value) Handles.DrawLine(p1, p2, 2);
                if(i == highlightEdgeId)
                {
                    var pt = ClosetPoint(p1, p2);
                    Handles.DrawSolidDisc(pt, Vector3.forward, 0.1f);
                }
            }
            
            if(showEdges.value && showInnerEdges.value)
            {
                edges.Clear();
                for(int i = 0; i < triangles.Length; i += 3)
                {
                    var a = triangles[i];
                    var b = triangles[i + 1];
                    var c = triangles[i + 2];
                    edges.Add(new Vector2Int(a, b));
                    edges.Add(new Vector2Int(b, c));
                    edges.Add(new Vector2Int(c, a));
                }
                Handles.color = Color.green;
                foreach(var e in edges) Handles.DrawLine(target.VertexWorldPos(e.x), target.VertexWorldPos(e.y), 1);
            }
            
        }

        
        static readonly List<Vector2> guiPoints = new();
        private void SyncSelection()
        {
            var prevVert = highlightVertexId;
            var prevEdge = highlightEdgeId;
            
            guiPoints.Clear();
            var temp = vertices
                .Select(x => x + target.transform.position)
                .Select(HandleUtility.WorldToGUIPoint);
            guiPoints.AddRange(temp);
            
            // 获取最近的点.
            float minDistance = pointSelectionRangeInPixel;
            highlightVertexId = null;
            for(int i = 0; i < guiPoints.Count; i++)
            {
                var p = guiPoints[i];
                var distance = Vector2.Distance(Event.current.mousePosition, p);
                if(distance < minDistance)
                {
                    highlightVertexId = i;
                    minDistance = distance;
                }
            }
            if(prevVert != highlightVertexId) needRepaint = true;
            
            minDistance = edgeSelectionRangeInPixel;
            highlightEdgeId = null;
            if(highlightVertexId != null)
            {
                if(prevEdge != highlightEdgeId) needRepaint = true;
                return;
            }
            
            // 获取最近的边. 边围成一个多边形.
            for(int i = 0; i < guiPoints.Count; i++)
            {
                var p1 = guiPoints[i];
                var p2 = guiPoints[(i + 1).Repeat(guiPoints.Count)];
                var mousePos = Event.current.mousePosition;
                var d = HandleUtility.DistancePointToLineSegment(mousePos, p1, p2);
                if(d < minDistance)
                {
                    highlightEdgeId = i;
                    minDistance = d;
                }
            }
            if(prevEdge != highlightEdgeId) needRepaint = true;
        }
    }
}
